/*
 * File:    Dynaload.java
 * Created: 12-Mar-2006
 * Version: $Id: //depot/bitgate/bsse/src/bitgate/util/dynaload/Dynaload.java#1 $
 *
 * by Kenji Hollis <kenji@bitgatesoftware.com>
 * Released under the Creative Commons License version 2.5
 */

package com.bitgate.util.dynaload;

import static com.bitgate.util.wildcard.Wildcard.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * The Dynamic Loader class is used to dynamically load classes from a jar file or network stream, and make their
 * class methods available without having to specify them in the classpath at initialization time.
 * <p/>
 * This allows for several distinct features:
 * <p/>
 * <li> It allows one to load in a JAR file on the fly, and instantiate objects from within that file on the fly.</li>
 * <li> It allows a class to be unloaded from a JAR cache. </li>
 * <li> It allows for JAR files to be changed at any time (or network streams for that matter) so that classes can be
 * reloaded on the fly, without requiring a restart of the currently running program. </li>
 * <p/>
 * This class was designed to help people design programs that can be used in a critical environment, or one that requires
 * a maximum allowed uptime.  There are several points that should be discussed when using this <code>ClassLoader</code>
 * object, and they must be followed explicitly.
 * <p/>
 * You cannot instantiate a class with an interface that is specified within the JAR file that the class was instantiated
 * from.  The interface class <b>must be</b> a part of the system or base class, and the JAR file that contains that class
 * must implement the interface in the form that exists in the currently running JRE.  There are no exceptions (AFAIK) to
 * this rule.
 * <p/>
 * This code is a very simplified <code>ClassLoader</code> object.  It is not intended to be used as a heavyweight
 * <code>ClassLoader</code> object, it is strictly used for loading in classes on the fly, and resolving unknown class
 * names from jar files that are loaded in.  <code>Dynaload</code> cannot be applied against stream objects.  For that,
 * you need to use the <code>StreamDynaload ClassLoader</code> class.
 * <p/>
 * TODO: MAKE THIS CODE THREADSAFE.
 * TODO: SYNCHRONIZE ONLY THE FUNCTIONS THAT NEED SYNCHRONIZATION.
 * TODO: DOES THIS CODE NEED TO BE SYNCHRONIZED AT ALL?
 *
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @since 0.1
 */
public class Dynaload extends ClassLoader
{
    private final ZipFile zFile;
    private final HashMap<String, Object> classHash;
    private static final Object NOT_AVAILABLE = new Object();

    /*
     * TODO:
     *   - Add a function to allow for dynamically loading from a remote URL (http/https)
     *   - Modify _loadClass so it does the requesting of said data.
     */

    /**
     * This starts up the dynamic loader to monitor class files from the specified filename.
     * <p/>
     * <u>Note:</u> JAR files that contain large amounts of class files will slow down the system when loading in the
     * classes.  Any system-wide class files that are named exactly the same as the classes loaded from the JAR file
     * specified here will most likely clash and cause a <code>ClassCastException</code> upon instantiation.
     * 
     * @param filename The JAR filename to load.
     * @throws ClassNotFoundException on any errors during loading or caching.
     */
    public Dynaload(String filename) throws ClassNotFoundException
    {
		try {
		    this.zFile = new ZipFile(filename);
		} catch(IOException e) {
		    throw new ClassNotFoundException(filename);
		}
	
		this.classHash = new HashMap<String, Object>();
	
		for(Enumeration<?> e = this.zFile.entries(); e.hasMoreElements();) {
		    ZipEntry zEntry = (ZipEntry) e.nextElement();
		    String fname;
	
		    if ((fname = zEntry.getName()).endsWith("class")) {
				if (fname.startsWith("/")) {
				    fname = fname.substring(1);
				}
		
				this.classHash.put(fname.replaceAll("/", ".").substring(0, fname.lastIndexOf(".")), this);
		    }
		}
    }

    /**
     * Loads in the specified class name without system-wide resolution.
     *
     * @param className The classname to load.
     * @throws ClassNotFoundException on any errors.
     * @return <code>Class&lt;?&gt;</code> object.
     */
    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException
    {
    	return loadClass(className, false);
    }

    /**
     * Loads in the specified class name.
     *
     * @param className The classname to load.
     * @param resolveIt Flag indicating whether or not to resolve the class name.
     * @throws ClassNotFoundException on any errors.
     * @return <code>Class&lt;?&gt;</code> object.
     */
    @Override
    public Class<?> loadClass(String className, boolean resolveIt) throws ClassNotFoundException
    {
		Object obj = this.classHash.get(className);
	
		if (obj == NOT_AVAILABLE) {
		    throw new ClassNotFoundException(className);
		} else if (obj instanceof Dynaload) {
		    Dynaload dynClassLoader = (Dynaload) obj;
		    return dynClassLoader._loadClass(className, resolveIt);
		}
	
		try {	
		    Class<?> cls;
		    ClassLoader parentLoader = getClass().getClassLoader();
	
		    // If a loader was found, load the class, otherwise find the system class.
		    cls = (parentLoader != null) ? parentLoader.loadClass(className) : findSystemClass(className);
	
		    return cls;
		} catch(ClassNotFoundException cnf) {
		    this.classHash.put(className, NOT_AVAILABLE);
		    throw cnf;
		} catch(NullPointerException cnf) {
		    throw new ClassNotFoundException(className + ": Not loadable, and not found as a system class.");
		}
    }

    /**
     * Instantiates a new copy of the specified class name.
     *
     * @param className The name of the class to instantiate.
     * @param resolveIt Flag indicating whether or not the class should be resolved.
     * @throws ClassNotFoundException if the class was not found
     * @throws InstantiationException if the class could not be instantiated
     * @throws IllegalAccessException if an illegal attempt to access a class occurred (ie. private/protected)
     * @return <code>Object</code> after instantiation.
     */
    public Object instantiate(String className, boolean resolveIt) throws ClassNotFoundException, InstantiationException,
        IllegalAccessException
    {
		Class<?> cls = loadClass(className, resolveIt);
	
		return (cls != null) ? cls.newInstance() : null;
    }

    /**
     * Returns a list of the known supported classes, filtered by name.  Any classes that are loaded as a result of system
     * classes being read in are not included in this list.
     *
     * @param match The wildcard match string.
     * @return <code>String[]</code> containing a list of loaded classes.
     */
    public String[] getCachedClasses(String match)
    {
		if (this.classHash == null || this.classHash.keySet().iterator() == null) {
		    return null;
		}
	
		boolean isGlobal;
	
		isGlobal = (match != null && "*".equals(match));
	
		String matchString = toRegexp(match);
		Iterator<String> classEntries = this.classHash.keySet().iterator();
		String[] cachedSet = new String[this.classHash.size()];
		int i = 0;
	
		while(classEntries.hasNext()) {
		    boolean addable = isGlobal;
		    String classEntryString = classEntries.next();
	
		    addable = (!addable && classEntryString.matches(matchString));
	
		    if (addable) {
		    	cachedSet[i++] = classEntryString;
		    }
		}
	
		return cachedSet;
    }

    /**
     * Returns a list of all of the known supported classes.
     *
     * @return <code>String[]</code> containing a list of loaded classes.
     */
    public String[] getCachedClasses()
    {
    	return getCachedClasses("*");
    }

    // TODO: Ugh.  Fix me, fix me, fix me.  The entire code is synchronized as is the entire functional block!  YUCK!
    private synchronized Class<?> _loadClass(String className, boolean resolveIt) throws ClassNotFoundException
    {
	    Class<?> cls = findLoadedClass(className);

	    if (cls != null) {
			if (resolveIt) {
			    resolveClass(cls);
			}
	
			return cls;
	    }

	    try {
			String filename = className.replaceAll("\\.", "/") + ".class";
			ZipEntry entry = this.zFile.getEntry(filename);
			
			if (entry == null) {
			    throw new ClassNotFoundException(className);
			}
	
			InputStream in = this.zFile.getInputStream(entry);
			int len = (int) entry.getSize();
			byte[] data = new byte[len];
			int success = 0;
			int offset = 0;
	
			while(success < len) {
			    len -= success;
			    offset += success;
			    success = in.read(data, offset, len);
	
			    if (success == -1) {
			    	throw new ClassNotFoundException(className + " from zip file '" + filename + "'");
			    }
			}
	
			cls = defineClass(className, data, 0, data.length);
	
			if (resolveIt) {
			    resolveClass(cls);
			}
	
			return cls;
	    } catch(IOException e) {
			throw new ClassNotFoundException(className);
	    }
    }
}
